﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Sustainalytics.ScreeningTool.Synchronization
{
    public static class AsyncExtensions
    {
        /// <summary>
        /// Concurrently Executes async actions for each item of <see cref="IEnumerable{T}"/>
        /// </summary>
        /// <typeparam name="T">Type of IEnumerable</typeparam>
        /// <param name="enumerable">instance of <see cref="IEnumerable{T}"/></param>
        /// <param name="action">an async <see cref="Action" /> to execute</param>
        /// <param name="maxDegreeOfParallelism">Optional, max numbers of the actions to run in parallel,
        /// Must be grater than 0</param>
        /// <returns>A Task representing an async operation</returns>
        /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
        public static async Task ParallelForEachAsync<T>(
            this IEnumerable<T> enumerable,
            Func<T, Task> action,
            int? maxDegreeOfParallelism = null)
        {
            if (!maxDegreeOfParallelism.HasValue)
            {
                await Task.WhenAll(enumerable.Select(item => action(item)));
                return;
            }

            using (var throttler = new SemaphoreSlim(maxDegreeOfParallelism.Value, maxDegreeOfParallelism.Value))
            {
                var tasks = enumerable.Select(async item =>
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await throttler.WaitAsync().ConfigureAwait(false);

                    try
                    {
                        await action(item).ConfigureAwait(false);
                    }
                    finally
                    {
                        // action is completed, so decrement the number of currently running tasks
                        throttler.Release();
                    }
                });

                // Wait for all of the provided tasks to complete.
                await Task.WhenAll(tasks.ToArray());
            }
        }
    }
}
